0%

转载,原文地址 https://mp.weixin.qq.com/s/Dh3G0e3IhGCmtQI1K0mRiA

每天早上七点三十,准时推送干货

最近公司来了一批实习生,阿粉负责带一个。这位小师弟说实话,基本功很扎实,做事也非常靠谱,深得阿粉真传。

不过最近给其 Review 代码的时候,阿粉发现小师弟有些代码逻辑有些繁琐,有些代码阿粉看来可以用一些开源工具类实现,不需要自己重复实现。

不过这也是正常的,阿粉刚入行的时候写的代码也是这样,这几年慢慢接触了一些开源工具类,逐渐积累。现在写代码才会直接用工具类替换自己实现的这些繁琐的逻辑。

于是阿粉给小师弟分享了几个自己常用的开源工具类,小师弟学完直呼:『666』。

这里阿粉抛砖引玉,分享几个常用的工具类,希望帮助到刚入行的同学们。其他编程老司机如果还有其他好用的工具类,欢迎评论区分享。

下文主要分享这几个方向的常用工具类:

字符串相关工具类

Java 中 String 应该是日常用的最多一个类吧,平常我们很多代码需要围绕 String ,做一些处理。

JDK 提供 String API 虽然比较多,但是功能比较基础,通常我们需要结合 String 多个方法才能完成一个业务功能。

下面介绍一下 Apache 提供的一个工具类 StringUtils.

Maven Pom 信息如下:

1
2
3
4
5
<dependency>
    <groupId>org.apache.commons</groupId>
    <artifactId>commons-lang3</artifactId>
    <version>3.10</version>
</dependency>

commons-lang 有两个版本,一个是 commons-lang3 ,一个是 commons-lang 。

commons-lang 是老版本,已经很久没有维护了。

commons-lang3 是一直在维护的版本,推荐直接使用这个版本。

注意:如果你系统已经有 commons-lang,注意如果直接替换成 commons-lang3,将会编译错误。commons-lang3 中相关类与 commons-lang 一样,但是包名不一样。

判断字符串是否为空

判断字符串是否为空,想必每个人应该都写过吧:

1
2
3
if (null == str || str.isEmpty()) {

}

虽然这段代码非常简单,但是说实话,阿粉以前还是在这里犯过空指针的异常的。

使用 StringUtils ,上面代码可以替换下面这样:

1
2
3
if (StringUtils.isEmpty(str)) {

}

StringUtils 内部还有一个方法 isBlank,也是用来判断字符串是否为空,两个方法比较相近,比较搞混,主要区别如下:

1
2
3
// 如果字符串都是空格的话,
StringUtils.isBlank(" ")       = true;
StringUtils.isEmpty(" ")       = false;

判断字符串是否为空,使用频率非常高,这里大家可以使用 IDEA Prefix 的功能,输入直接生成判空语句。

字符串固定长度

这个通常用于字符串需要固定长度的场景,比如需要固定长度字符串作为流水号,若流水号长度不足,,左边补 0 。

这里当然可以使用 String#format 方法,不过阿粉觉得比较麻烦,这里可以这样使用:

1
2
// 字符串固定长度 8位,若不足,往左补 0
StringUtils.leftPad("test", 8, "0");

另外还有一个 StringUtils#rightPad, 这个方法与上面方法正好相反。

字符串关键字替换

StringUtils 提供一些列的方法,可以替换某些关键字:

1
2
3
4
5
6
7
// 默认替换所有关键字
StringUtils.replace("aba", "a", "z")   = "zbz";
// 替换关键字,仅替换一次
StringUtils.replaceOnce("aba", "a", "z")   = "zba";
// 使用正则表达式替换
StringUtils.replacePattern("ABCabc123", "[^A-Z0-9]+", "")   = "ABC123";
....

字符串拼接

字符串拼接是个常见的需求,简单办法使用 StringBuilder 循环遍历拼接:

1
2
3
4
5
6
7
8
9
10
11
String[] array = new String[]{"test", "1234", "5678"};
StringBuilder stringBuilder = new StringBuilder();

for (String s : array) {
    stringBuilder.append(s).append(";");
}
// 防止最终拼接字符串为空 
if (stringBuilder.length() > 0) {
    stringBuilder.deleteCharAt(stringBuilder.length() - 1);
}
System.out.println(stringBuilder.toString());

上面业务代码不太难,但是需要注意一下上面这段代码非常容易出错,容易抛出 StringIndexOutOfBoundsException

这里我们可以直接使用以下方法获取拼接之后字符串:

1
StringUtils.join(["a", "b", "c"], ",")    = "a,b,c"

StringUtils 只能传入数组拼接字符串,不过我比较喜欢集合拼接,所以再推荐下 Guava 的 Joiner

实例代码如下:

1
2
3
4
5
6
7
8
9
10
11
String[] array = new String[]{"test", "1234", "5678"};
List<String> list=new ArrayList<>();
list.add("test");
list.add("1234");
list.add("5678");
StringUtils.join(array, ",");

// 逗号分隔符,跳过 null
Joiner joiner=Joiner.on(",").skipNulls();
joiner.join(array);
joiner.join(list);

字符串拆分

有字符串拼接,就会有拆分字符串的需求,同样的 StringUtils 也有拆分字符串的方法。

1
2
StringUtils.split("a..b.c", '.')   = ["a", "b", "c"]
StringUtils.splitByWholeSeparatorPreserveAllTokens("a..b.c", ".")= ["a","", "b", "c"]

ps: 注意以上两个方法区别。

StringUtils 拆分之后得到是一个数组,我们可以使用 Guava 的

1
2
3
4
5
Splitter splitter = Splitter.on(",");
// 返回是一个 List 集合,结果:[ab, , b, c]
splitter.splitToList("ab,,b,c");
// 忽略空字符串,输出结果 [ab, b, c]
splitter.omitEmptyStrings().splitToList("ab,,b,c")

StringUtils 内部还有其他常用的方法,小伙伴可以自行查看其 API。

日期相关工具类

DateUtils/DateFormatUtils

JDK8 之前,Java 只提供一个 Date 类,平常我们需要将 Date 按照一定格式转化成字符串,我们需要使用 SimpleDateFormat

1
2
3
4
5
SimpleDateFormat simpleDateFormat=new SimpleDateFormat("yyyy-MM-dd HH:mm:ss");
// Date 转 字符串
simpleDateFormat.format(new Date());
// 字符串 转 Date
simpleDateFormat.parse("2020-05-07 22:00:00");

代码虽然简单,但是这里需要注意 SimpleDateFormat,不是线程安全的,多线程环境一定要注意使用安全。

这里阿粉推荐 commons-lang3 下的时间工具类DateUtils/DateFormatUtils, 解决 Date 与字符串转化问题。

ps: 吐槽一下,你们工程中有没有多个叫 DateUtils 类?阿粉发现我们现有工程,多个模块有提供这个类,每个实现大同小异。

使用方法非常简单:

1
2
3
4
// Date 转化为字符串
DateFormatUtils.format(new Date(),"yyyy-MM-dd HH:mm:ss");
// 字符串 转 Date
DateUtils.parseDate("2020-05-07 22:00:00","yyyy-MM-dd HH:mm:ss");

除了格式转化之外,DateUtils 还提供时间计算的相关功能。

1
2
3
4
5
6
7
8
9
10
11
12
Date now = new Date();
// Date 加 1 天
Date addDays = DateUtils.addDays(now, 1);
// Date 加 33 分钟
Date addMinutes = DateUtils.addMinutes(now, 33);
// Date 减去 233 秒
Date addSeconds = DateUtils.addSeconds(now, -233);
// 判断是否 Wie 同一天
boolean sameDay = DateUtils.isSameDay(addDays, addMinutes);
// 过滤时分秒,若 now 为 2020-05-07 22:13:00 调用 truncate 方法以后
// 返回时间为 2020-05-07 00:00:00
Date truncate = DateUtils.truncate(now, Calendar.DATE);

JDK8 时间类

JDK8 之后,Java 将日期与时间分为 LocalDateLocalTime,功能定义更加清晰,当然其也提供一个 LocalDateTime,包含日期与时间。这些类相对于 Date 类优点在于,这些类与 String 类一样都是不变类型,不但线程安全,而且不能修改。

ps:仔细对比 mysql 时间日期类型 DATETIMEDATETIME,有没有感觉差不多

现在 mybatis 等 ORM 框架已经支持 LocalDate 与 JDBC 时间类型转化,所以大家可以直接将时间字段实际类型定义为 JDK8 时间类型,然后再进行相关转化。

如果依然使用的是 Date 类型,如果需要使用新的时间类型,我们需要进行相关转化。两者之间进行转化, 稍微复杂一点,我们需要显示指定当前时区。

1
2
3
4
5
Date now = new Date();
// Date-----> LocalDateTime 这里指定使用当前系统默认时区
LocalDateTime localDateTime = now.toInstant().atZone(ZoneId.systemDefault()).toLocalDateTime();
// LocalDateTime------> Date 这里指定使用当前系统默认时区
Date date = Date.from(localDateTime.atZone(ZoneId.systemDefault()).toInstant());

接下来我们使用 LocalDateTime 进行字符串格式化。

1
2
3
4
// 按照 yyyy-MM-dd HH:mm:ss 转化时间
LocalDateTime dateTime = LocalDateTime.parse("2020-05-07 22:34:00", DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss"));
// 将 LocalDateTime 格式化字符串
String format = DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss").format(dateTime);

另外我们使用 LocalDateTime 获取当前时间年份,月份特别简单:

1
2
3
4
5
6
7
LocalDateTime now = LocalDateTime.now();
// 年
int year = now.getYear();
// 月
int month = now.getMonthValue();
// 日
int day = now.getDayOfMonth();

最后我们还可以使用 LocalDateTime 进行日期加减,获取下一天的时间:

1
2
3
4
5
6
LocalDateTime now = LocalDateTime.now();
// 当前时间加一天
LocalDateTime plusDays = now.plusDays(1l);
// 当前时间减一个小时
LocalDateTime minusHours = now.minusHours(1l);
// 还有很多其他方法

总之 JDK8 提供的时间类非常好用,还没用过小伙伴,可以尝试下。

集合 / 数组相关

集合与数组我们日常也需要经常使用,也需要对其进行判空:

1
2
3
if (null == list || list.isEmpty()) {

}

ps: 数组、Map 集合与其类似

上面代码如字符串判空一样写起来都非常简单,但是也比较容易写出会抛出空指针异常的代码。这里我们可以使用 commons-collections 提供工具类。

pom 信息:

1
2
3
4
5
<dependency>
    <groupId>org.apache.commons</groupId>
    <artifactId>commons-collections4</artifactId>
    <version>4.4</vesion>
</dependency>

ps: 还有一个低版本的 ,artifactId 为 commons-collections

我们可以使用 CollectionUtils/MapUtils进行判空判断。

1
2
3
4
5
6
7
8
// List/Set 集合判空
if(CollectionUtils.isEmpty(list)){

}
// Map 等集合进行判空
if (MapUtils.isEmpty(map)) {
    
}

至于数组判空判断需要使用 commons-lang 下的 ArrayUtils进行判断:

1
2
3
4
// 数组判空
if (ArrayUtils.isEmpty(array)) {
    
}

除此之外还有一些列的对于集合增强方法,比如快速将数组加入到现有集合中:

1
2
3
4
5
6
List<String> listA = new ArrayList<>();
listA.add("1");
listA.add("2");
listA.add("3");
String[] arrays = new String[]{"a", "b", "c"};
CollectionUtils.addAll(listA, arrays);

其他方法感兴趣同学可以再自行研究下,另外 Guava 中也有提供对于集合的操作增强类 Lists/Maps, 这个可以看下阿粉之前写的:老司机阿粉带你玩转 Guava 集合类

I/O 相关

JDK 有提供一系列的类可以读取文件等,不过阿粉觉得那些类有些晦涩难懂,实现一个小功能可能还要写好多代码,而且还不一定能写对。

阿粉推荐一下 Apache 提供的 commons-io 库,增强 I/O 操作,简化操作难度。pom 信息:

1
2
3
4
5
<dependency>
    <groupId>commons-io</groupId>
    <artifactId>commons-io</artifactId>
    <version>2.6</version>
</dependency>

FileUtils - 文件操作工具类

文件操作工具类提供一系列方法,可以让我们快速读取写入文件。

快速实现文件 / 文件夹拷贝操作 ,FileUtils.copyDirectory/FileUtils.copyFile

1
2
3
4
// 拷贝文件
File fileA = new File("E:\\test\\test.txt");
File fileB = new File("E:\\test1\\test.txt");
FileUtils.copyFile(fileA,fileB);

使用 FileUtils.listFiles 获取指定文件夹上所有文件

1
2
3
// 按照指定文件后缀如java,txt等去查找指定文件夹的文件
File directory = new File("E:\\test");
FileUtils.listFiles(directory, new String[]{"txt"}, false);

使用 FileUtils.readLines 读取该文件所有行。

1
2
// 读取指定文件所有行 不需要使用 while 循环读取流了
List<String> lines = FileUtils.readLines(fileA)

有读就存在写,可以使用 FileUtils.writeLines,直接将集合中数据,一行行写入文本。

1
2
3
4
// 可以一行行写入文本
List<String> lines = new ArrayList<>();
.....
FileUtils.writeLines(lines)

IOUtils-I/O 操作相关工具类

FileUtils 主要针对相关文件操作,IOUtils 更加针对底层 I/O, 可以快速读取 InputStream。实际上 FileUtils 底层操作依赖就是 IOUtils

IOUtils可以适用于一个比较试用的场景,比如支付场景下,HTTP 异步通知场景。如果我们使用 JDK 原生方法写:

从 Servlet 获取异步通知内容

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
byte[] b = null;
ByteArrayOutputStream baos = null;
String respMsg = null;
try {
    byte[] buffer = new byte[1024];
    baos = new ByteArrayOutputStream();
   // 获取输入流
    InputStream in = request.getInputStream();
    for (int len = 0; (len = in.read(buffer)) > 0; ) {
        baos.write(buffer, 0, len);
    }
    b = baos.toByteArray();
    baos.close();
   // 字节数组转化成字符串
    String reqMessage = new String(b, "utf-8");
} catch (IOException e) {
  
} finally {
    if (baos != null) {
        try {
            baos.close();
        } catch (IOException e) {
           
        }
    }
}

上面代码说起来还是挺复杂的。不过我们使用 IOUtils,一个方法就可以简单搞定:

1
2
3
4
// 将输入流信息全部输出到字节数组中
byte[] b = IOUtils.toByteArray(request.getInputStream());
// 将输入流信息转化为字符串
String resMsg = IOUtils.toString(request.getInputStream());

ps: InputStream 不能被重复读取

计时

编程中有时需要统计代码的的执行耗时,当然执行代码非常简单,结束时间与开始时间相减即可。

1
2
3
4
5
6
7
long start = System.currentTimeMillis();   //获取开始时间

//其他代码
//...
long end = System.currentTimeMillis(); //获取结束时间

System.out.println("程序运行时间: " + (end - start) + "ms");

虽然代码很简单,但是非常不灵活,默认情况我们只能获取 ms 单位,如果需要转换为秒,分钟,就需要另外再计算。

这里我们介绍 Guava Stopwatch 计时工具类,借助他统计程序执行时间,使用方式非常灵活。

commons-lang3 与 Spring-core 也有这个工具类,使用方式大同小异,大家根据情况选择。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
// 创建之后立刻计时,若想主动开始计时
Stopwatch stopwatch = Stopwatch.createStarted();
// 创建计时器,但是需要主动调用 start 方法开始计时
// Stopwatch stopwatch = Stopwatch.createUnstarted();
// stopWatch.start();
// 模拟其他代码耗时
TimeUnit.SECONDS.sleep(2l);

// 当前已经消耗的时间
System.out.println(stopwatch.elapsed(TimeUnit.SECONDS));;

TimeUnit.SECONDS.sleep(2l);

// 停止计时 未开始的计时器调用 stop 将会抛错 IllegalStateException
stopwatch.stop();
// 再次统计总耗时
System.out.println(stopwatch.elapsed(TimeUnit.SECONDS));;
// 重新开始,将会在原来时间基础计算,若想重新从 0开始计算,需要调用 stopwatch.reset()
stopwatch.start();
TimeUnit.SECONDS.sleep(2l);
System.out.println(stopwatch.elapsed(TimeUnit.SECONDS));

输出结果为:

总结

今天阿粉抛砖引玉,介绍了字符串、日期、数组 / 集合、I/O、计时等工具类,简化日常业务代码。大家看完可以尝试一下,不得不说,这些工具类真香!

如果大家喜欢我们的文章,欢迎大家转发,点击在看让更多的人看到。也欢迎大家热爱技术和学习的朋友加入的我们的知识星球当中,我们共同成长,进步。

往期精彩回顾

这回,阿粉要将 synchronized 与锁的关系讲透!!!

教你一招,如何使用抓包工具轻松获取微信文章数据!

龙岭迷窟真的这么好看?今天我们就用 Java 爬取豆瓣数据好好分析一下!

转载, 原文地址 https://mp.weixin.qq.com/s/O61fUvK9obfMjxOtPnT0eA

0. 前言

本文已经收录进 75K Star 的 Java 开源项目 JavaGuide:https://github.com/Snailclimb/JavaGuide

可以毫不夸张地说,这篇文章介绍的 Spring/SpringBoot 常用注解基本已经涵盖你工作中遇到的大部分常用的场景。对于每一个注解我都说了具体用法,掌握搞懂,使用 SpringBoot 来开发项目基本没啥大问题了!

为什么要写这篇文章?

最近看到网上有一篇关于 SpringBoot 常用注解的文章被转载的比较多,我看了文章内容之后属实觉得质量有点低,并且有点会误导没有太多实际使用经验的人(这些人又占据了大多数)。所以,自己索性花了大概 两天时间简单总结一下了。

整个目录如下,内容有点多:

因为我个人的能力和精力有限,如果有任何不对或者需要完善的地方,请帮忙指出!Guide 哥感激不尽!

1. @SpringBootApplication

这里先单独拎出@SpringBootApplication 注解说一下,虽然我们一般不会主动去使用它。

Guide 哥:这个注解是 Spring Boot 项目的基石,创建 SpringBoot 项目之后会默认在主类加上。

1
2
3
4
5
6
@SpringBootApplication
public class SpringSecurityJwtGuideApplication {
      public static void main(java.lang.String[] args) {
        SpringApplication.run(SpringSecurityJwtGuideApplication.class, args);
    }
}

我们可以把 @SpringBootApplication看作是 @Configuration@EnableAutoConfiguration@ComponentScan 注解的集合。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
package org.springframework.boot.autoconfigure;
@Target(ElementType.TYPE)
@Retention(RetentionPolicy.RUNTIME)
@Documented
@Inherited
@SpringBootConfiguration
@EnableAutoConfiguration
@ComponentScan(excludeFilters = {
  @Filter(type = FilterType.CUSTOM, classes = TypeExcludeFilter.class),
  @Filter(type = FilterType.CUSTOM, classes = AutoConfigurationExcludeFilter.class) })
public @interface SpringBootApplication {
   ......
}

package org.springframework.boot;
@Target(ElementType.TYPE)
@Retention(RetentionPolicy.RUNTIME)
@Documented
@Configuration
public @interface SpringBootConfiguration {

}

根据 SpringBoot 官网,这三个注解的作用分别是:

  • @EnableAutoConfiguration:启用 SpringBoot 的自动配置机制

  • @ComponentScan: 扫描被@Component (@Service,@Controller) 注解的 bean,注解默认会扫描该类所在的包下所有的类。

  • @Configuration:允许在 Spring 上下文中注册额外的 bean 或导入其他配置类

2. Spring Bean 相关

2.1. @Autowired

自动导入对象到类中,被注入进的类同样要被 Spring 容器管理比如:Service 类注入到 Controller 类中。

1
2
3
4
5
6
7
8
9
10
11
12
@Service
public class UserService {
  ......
}

@RestController
@RequestMapping("/users")
public class UserController {
   @Autowired
   private UserService userService;
   ......
}

2.2. Component,@Repository,@Service, @Controller

我们一般使用 @Autowired 注解让 Spring 容器帮我们自动装配 bean。要想把类标识成可用于 @Autowired 注解自动装配的 bean 的类, 可以采用以下注解实现:

  • @Component :通用的注解,可标注任意类为 Spring 组件。如果一个 Bean 不知道属于哪个层,可以使用@Component 注解标注。

  • @Repository : 对应持久层即 Dao 层,主要用于数据库相关操作。

  • @Service : 对应服务层,主要涉及一些复杂的逻辑,需要用到 Dao 层。

  • @Controller : 对应 Spring MVC 控制层,主要用户接受用户请求并调用 Service 层返回数据给前端页面。

2.3. @RestController

@RestController注解是@Controller和@ResponseBody的合集, 表示这是个控制器 bean, 并且是将函数的返回值直 接填入 HTTP 响应体中, 是 REST 风格的控制器。

Guide 哥:现在都是前后端分离,说实话我已经很久没有用过@Controller。如果你的项目太老了的话,就当我没说。

单独使用 @Controller 不加 @ResponseBody的话一般使用在要返回一个视图的情况,这种情况属于比较传统的 Spring MVC 的应用,对应于前后端不分离的情况。@Controller +@ResponseBody 返回 JSON 或 XML 形式数据

关于@RestController@Controller的对比,请看这篇文章:@RestController vs @Controller

2.4. @Scope

声明 Spring Bean 的作用域,使用方法:

1
2
3
4
5
@Bean
@Scope("singleton")
public Person personSingleton() {
    return new Person();
}

四种常见的 Spring Bean 的作用域:

  • singleton : 唯一 bean 实例,Spring 中的 bean 默认都是单例的。

  • prototype : 每次请求都会创建一个新的 bean 实例。

  • request : 每一次 HTTP 请求都会产生一个新的 bean,该 bean 仅在当前 HTTP request 内有效。

  • session : 每一次 HTTP 请求都会产生一个新的 bean,该 bean 仅在当前 HTTP session 内有效。

2.5. Configuration

一般用来声明配置类,可以使用 @Component注解替代,不过使用Configuration注解声明配置类更加语义化。

1
2
3
4
5
6
7
8
@Configuration
public class AppConfig {
    @Bean
    public TransferService transferService() {
        return new TransferServiceImpl();
    }

}

3. 处理常见的 HTTP 请求类型

5 种常见的请求类型:

  • GET :请求从服务器获取特定资源。举个例子:GET /users(获取所有学生)

  • POST :在服务器上创建一个新的资源。举个例子:POST /users(创建学生)

  • PUT :更新服务器上的资源(客户端提供更新后的整个资源)。举个例子:PUT /users/12(更新编号为 12 的学生)

  • DELETE :从服务器删除特定的资源。举个例子:DELETE /users/12(删除编号为 12 的学生)

  • PATCH :更新服务器上的资源(客户端提供更改的属性,可以看做作是部分更新),使用的比较少,这里就不举例子了。

3.1. GET 请求

@GetMapping("users") 等价于@RequestMapping(value="/users",method=RequestMethod.GET)

1
2
3
4
@GetMapping("/users")
public ResponseEntity<List<User>> getAllUsers() {
 return userRepository.findAll();
}

3.2. POST 请求

@PostMapping("users") 等价于@RequestMapping(value="/users",method=RequestMethod.POST)

关于@RequestBody注解的使用,在下面的 “前后端传值” 这块会讲到。

1
2
3
4
@PostMapping("/users")
public ResponseEntity<User> createUser(@Valid @RequestBody UserCreateRequest userCreateRequest) {
 return userRespository.save(user);
}

3.3. PUT 请求

@PutMapping("/users/{userId}") 等价于@RequestMapping(value="/users/{userId}",method=RequestMethod.PUT)

1
2
3
4
5
@PutMapping("/users/{userId}")
public ResponseEntity<User> updateUser(@PathVariable(value = "userId") Long userId,
  @Valid @RequestBody UserUpdateRequest userUpdateRequest) {
  ......
}

3.4. DELETE 请求

@DeleteMapping("/users/{userId}")等价于@RequestMapping(value="/users/{userId}",method=RequestMethod.DELETE)

1
2
3
4
@DeleteMapping("/users/{userId}")
public ResponseEntity deleteUser(@PathVariable(value = "userId") Long userId){
  ......
}

3.5. PATCH 请求

一般实际项目中,我们都是 PUT 不够用了之后才用 PATCH 请求去更新数据。

1
2
3
4
5
  @PatchMapping("/profile")
  public ResponseEntity updateStudent(@RequestBody StudentUpdateRequest studentUpdateRequest) {
        studentRepository.updateDetail(studentUpdateRequest);
        return ResponseEntity.ok().build();
    }

4. 前后端传值

掌握前后端传值的正确姿势,是你开始 CRUD 的第一步!

4.1. @PathVariable@RequestParam

@PathVariable用于获取路径参数,@RequestParam用于获取查询参数。

举个简单的例子:

1
2
3
4
5
6
@GetMapping("/klasses/{klassId}/teachers")
public List<Teacher> getKlassRelatedTeachers(
         @PathVariable("klassId") Long klassId,
         @RequestParam(value = "type", required = false) String type ) {
...
}

如果我们请求的 url 是:/klasses/{123456}/teachers?type=web

那么我们服务获取到的数据就是:klassId=123456,type=web

4.2. @RequestBody

用于读取 Request 请求(可能是 POST,PUT,DELETE,GET 请求)的 body 部分并且 Content-Type 为 application/json 格式的数据,接收到数据之后会自动将数据绑定到 Java 对象上去。系统会使用HttpMessageConverter或者自定义的HttpMessageConverter将请求的 body 中的 json 字符串转换为 java 对象。

我用一个简单的例子来给演示一下基本使用!

我们有一个注册的接口:

1
2
3
4
5
@PostMapping("/sign-up")
public ResponseEntity signUp(@RequestBody @Valid UserRegisterRequest userRegisterRequest) {
  userService.save(userRegisterRequest);
  return ResponseEntity.ok().build();
}

UserRegisterRequest对象:

1
2
3
4
5
6
7
8
9
10
11
12
@Data
@AllArgsConstructor
@NoArgsConstructor
public class UserRegisterRequest {
    @NotBlank
    private String userName;
    @NotBlank
    private String password;
    @FullName
    @NotBlank
    private String fullName;
}

我们发送 post 请求到这个接口,并且 body 携带 JSON 数据:

1
{"userName":"coder","fullName":"shuangkou","password":"123456"}

这样我们的后端就可以直接把 json 格式的数据映射到我们的 UserRegisterRequest 类上。

👉 需要注意的是:一个请求方法只可以有一个@RequestBody,但是可以有多个@RequestParam@PathVariable。 如果你的方法必须要用两个 @RequestBody来接受数据的话,大概率是你的数据库设计或者系统设计出问题了!

5. 读取配置信息

很多时候我们需要将一些常用的配置信息比如阿里云 oss、发送短信、微信认证的相关配置信息等等放到配置文件中。

下面我们来看一下 Spring 为我们提供了哪些方式帮助我们从配置文件中读取这些配置信息。

我们的数据源application.yml内容如下::

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
wuhan2020: 2020年初武汉爆发了新型冠状病毒,疫情严重,但是,我相信一切都会过去!武汉加油!中国加油!

my-profile:
  name: Guide哥
  email: koushuangbwcx@163.com

library:
  location: 湖北武汉加油中国加油
  books:
    - name: 天才基本法
      description: 二十二岁的林朝夕在父亲确诊阿尔茨海默病这天,得知自己暗恋多年的校园男神裴之即将出国深造的消息——对方考取的学校,恰是父亲当年为她放弃的那所。
    - name: 时间的秩序
      description: 为什么我们记得过去,而非未来?时间“流逝”意味着什么?是我们存在于时间之内,还是时间存在于我们之中?卡洛·罗韦利用诗意的文字,邀请我们思考这一亘古难题——时间的本质。
    - name: 了不起的我
      description: 如何养成一个新习惯?如何让心智变得更成熟?如何拥有高质量的关系? 如何走出人生的艰难时刻?

5.1. @value(常用)

使用 @Value("${property}") 读取比较简单的配置信息:

1
2
@Value("${wuhan2020}")
String wuhan2020;

5.2. @ConfigurationProperties(常用)

通过@ConfigurationProperties读取配置信息并与 bean 绑定。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
@Component
@ConfigurationProperties(prefix = "library")
class LibraryProperties {
    @NotEmpty
    private String location;
    private List<Book> books;

    @Setter
    @Getter
    @ToString
    static class Book {
        String name;
        String description;
    }
  省略getter/setter
  ......
}

你可以像使用普通的 Spring bean 一样,将其注入到类中使用。

5.3. PropertySource(不常用)

@PropertySource读取指定 properties 文件

1
2
3
4
5
6
7
8
9
10
@Component
@PropertySource("classpath:website.properties")

class WebSite {
    @Value("${url}")
    private String url;

  省略getter/setter
  ......
}

更多内容请查看我的这篇文章:《10 分钟搞定 SpringBoot 如何优雅读取配置文件?》 。

6. 参数校验

数据的校验的重要性就不用说了,即使在前端对数据进行校验的情况下,我们还是要对传入后端的数据再进行一遍校验,避免用户绕过浏览器直接通过一些 HTTP 工具直接向后端请求一些违法数据。

JSR(Java Specification Requests) 是一套 JavaBean 参数校验的标准,它定义了很多常用的校验注解,我们可以直接将这些注解加在我们 JavaBean 的属性上面,这样就可以在需要校验的时候进行校验了,非常方便!

校验的时候我们实际用的是 Hibernate Validator 框架。Hibernate Validator 是 Hibernate 团队最初的数据校验框架,Hibernate Validator 4.x 是 Bean Validation 1.0(JSR 303)的参考实现,Hibernate Validator 5.x 是 Bean Validation 1.1(JSR 349)的参考实现,目前最新版的 Hibernate Validator 6.x 是 Bean Validation 2.0(JSR 380)的参考实现。

SpringBoot 项目的 spring-boot-starter-web 依赖中已经有 hibernate-validator 包,不需要引用相关依赖。如下图所示(通过 idea 插件—Maven Helper 生成):

非 SpringBoot 项目需要自行引入相关依赖包,这里不多做讲解,具体可以查看我的这篇文章:《如何在 Spring/Spring Boot 中做参数校验?你需要了解的都在这里!》。

👉 需要注意的是: 所有的注解,推荐使用 JSR 注解,即javax.validation.constraints,而不是org.hibernate.validator.constraints

6.1. 一些常用的字段验证的注解

  • @NotEmpty 被注释的字符串的不能为 null 也不能为空

  • @NotBlank 被注释的字符串非 null,并且必须包含一个非空白字符

  • @Null 被注释的元素必须为 null

  • @NotNull 被注释的元素必须不为 null

  • @AssertTrue 被注释的元素必须为 true

  • @AssertFalse 被注释的元素必须为 false

  • @Pattern(regex=,flag=)被注释的元素必须符合指定的正则表达式

  • @Email 被注释的元素必须是 Email 格式。

  • @Min(value)被注释的元素必须是一个数字,其值必须大于等于指定的最小值

  • @Max(value)被注释的元素必须是一个数字,其值必须小于等于指定的最大值

  • @DecimalMin(value)被注释的元素必须是一个数字,其值必须大于等于指定的最小值

  • @DecimalMax(value) 被注释的元素必须是一个数字,其值必须小于等于指定的最大值

  • @Size(max=, min=)被注释的元素的大小必须在指定的范围内

  • @Digits (integer, fraction)被注释的元素必须是一个数字,其值必须在可接受的范围内

  • @Past被注释的元素必须是一个过去的日期

  • @Future 被注释的元素必须是一个将来的日期

  • ……

6.2. 验证请求体 (RequestBody)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
@Data
@AllArgsConstructor
@NoArgsConstructor
public class Person {

    @NotNull(message = "classId 不能为空")
    private String classId;

    @Size(max = 33)
    @NotNull(message = "name 不能为空")
    private String name;

    @Pattern(regexp = "((^Man$|^Woman$|^UGM$))", message = "sex 值不在可选范围")
    @NotNull(message = "sex 不能为空")
    private String sex;

    @Email(message = "email 格式不正确")
    @NotNull(message = "email 不能为空")
    private String email;

}

我们在需要验证的参数上加上了@Valid注解,如果验证失败,它将抛出MethodArgumentNotValidException

1
2
3
4
5
6
7
8
9
@RestController
@RequestMapping("/api")
public class PersonController {

    @PostMapping("/person")
    public ResponseEntity<Person> getPerson(@RequestBody @Valid Person person) {
        return ResponseEntity.ok().body(person);
    }
}

6.3. 验证请求参数 (Path Variables 和 Request Parameters)

一定一定不要忘记在类上加上 Validated 注解了,这个参数可以告诉 Spring 去校验方法参数。

1
2
3
4
5
6
7
8
9
10
@RestController
@RequestMapping("/api")
@Validated
public class PersonController {

    @GetMapping("/person/{id}")
    public ResponseEntity<Integer> getPersonByID(@Valid @PathVariable("id") @Max(value = 5,message = "超过 id 的范围了") Integer id) {
        return ResponseEntity.ok().body(id);
    }
}

更多关于如何在 Spring 项目中进行参数校验的内容,请看《如何在 Spring/Spring Boot 中做参数校验?你需要了解的都在这里!》这篇文章。

7. 全局处理 Controller 层异常

介绍一下我们 Spring 项目必备的全局处理 Controller 层异常。

相关注解:

  1. @ControllerAdvice : 注解定义全局异常处理类

  2. @ExceptionHandler : 注解声明异常处理方法

如何使用呢?拿我们在第 5 节参数校验这块来举例子。如果方法参数不对的话就会抛出MethodArgumentNotValidException,我们来处理这个异常。

1
2
3
4
5
6
7
8
9
10
11
12
@ControllerAdvice
@ResponseBody
public class GlobalExceptionHandler {

    /**
     * 请求参数异常处理
     */
    @ExceptionHandler(MethodArgumentNotValidException.class)
    public ResponseEntity<?> handleMethodArgumentNotValidException(MethodArgumentNotValidException ex, HttpServletRequest request) {
       ......
    }
}

更多关于 Spring Boot 异常处理的内容,请看我的这两篇文章:

  1. SpringBoot 处理异常的几种常见姿势

  2. 使用枚举简单封装一个优雅的 Spring Boot 全局异常处理!

8. JPA 相关

8.1. 创建表

@Entity声明一个类对应一个数据库实体。

@Table 设置表明

1
2
3
4
5
6
7
8
9
10
@Entity
@Table(name = "role")
public class Role {
    @Id
    @GeneratedValue(strategy = GenerationType.IDENTITY)
    private Long id;
    private String name;
    private String description;
    省略getter/setter......
}

8.2. 创建主键

@Id :声明一个字段为主键。

使用@Id声明之后,我们还需要定义主键的生成策略。我们可以使用 @GeneratedValue 指定主键生成策略。

1. 通过 @GeneratedValue直接使用 JPA 内置提供的四种主键生成策略来指定主键生成策略。

1
2
3
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;

JPA 使用枚举定义了 4 中常见的主键生成策略,如下:

Guide 哥:枚举替代常量的一种用法

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
public enum GenerationType {

    /**
     * 使用一个特定的数据库表格来保存主键
     * 持久化引擎通过关系数据库的一张特定的表格来生成主键,
     */
    TABLE,

    /**
     *在某些数据库中,不支持主键自增长,比如Oracle、PostgreSQL其提供了一种叫做"序列(sequence)"的机制生成主键
     */
    SEQUENCE,

    /**
     * 主键自增长
     */
    IDENTITY,

    /**
     *把主键生成策略交给持久化引擎(persistence engine),
     *持久化引擎会根据数据库在以上三种主键生成 策略中选择其中一种
     */
    AUTO
}

@GeneratedValue注解默认使用的策略是GenerationType.AUTO

1
2
3
4
5
public @interface GeneratedValue {

    GenerationType strategy() default AUTO;
    String generator() default "";
}

一般使用 MySQL 数据库的话,使用GenerationType.IDENTITY策略比较普遍一点(分布式系统的话需要另外考虑使用分布式 ID)。

2. 通过 @GenericGenerator声明一个主键策略,然后 @GeneratedValue使用这个策略

1
2
3
4
@Id
@GeneratedValue(generator = "IdentityIdGenerator")
@GenericGenerator(name = "IdentityIdGenerator", strategy = "identity")
private Long id;

等价于:

1
2
3
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;

jpa 提供的主键生成策略有如下几种:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
public class DefaultIdentifierGeneratorFactory
  implements MutableIdentifierGeneratorFactory, Serializable, ServiceRegistryAwareService {

 @SuppressWarnings("deprecation")
 public DefaultIdentifierGeneratorFactory() {
  register( "uuid2", UUIDGenerator.class );
  register( "guid", GUIDGenerator.class );   // can be done with UUIDGenerator + strategy
  register( "uuid", UUIDHexGenerator.class );   // "deprecated" for new use
  register( "uuid.hex", UUIDHexGenerator.class );  // uuid.hex is deprecated
  register( "assigned", Assigned.class );
  register( "identity", IdentityGenerator.class );
  register( "select", SelectGenerator.class );
  register( "sequence", SequenceStyleGenerator.class );
  register( "seqhilo", SequenceHiLoGenerator.class );
  register( "increment", IncrementGenerator.class );
  register( "foreign", ForeignGenerator.class );
  register( "sequence-identity", SequenceIdentityGenerator.class );
  register( "enhanced-sequence", SequenceStyleGenerator.class );
  register( "enhanced-table", TableGenerator.class );
 }

 public void register(String strategy, Class generatorClass) {
  LOG.debugf( "Registering IdentifierGenerator strategy [%s] -> [%s]", strategy, generatorClass.getName() );
  final Class previous = generatorStrategyToClassNameMap.put( strategy, generatorClass );
  if ( previous != null ) {
   LOG.debugf( "    - overriding [%s]", previous.getName() );
  }
 }

}

8.3. 设置字段类型

@Column 声明字段。

示例:

设置属性 userName 对应的数据库字段名为 user_name,长度为 32,非空

1
2
@Column(name = "user_name", nullable = false, length=32)
private String userName;

设置字段类型并且加默认值,这个还是挺常用的。

1
2
Column(columnDefinition = "tinyint(1) default 1")
private Boolean enabled;

8.4. 指定不持久化特定字段

@Transient :声明不需要与数据库映射的字段,在保存的时候不需要保存进数据库 。

如果我们想让secrect 这个字段不被持久化,可以使用 @Transient关键字声明。

1
2
3
4
5
6
7
8
Entity()
public class User {

    ......
    @Transient
    private String secrect; // not persistent because of @Transient

}

除了 @Transient关键字声明, 还可以采用下面几种方法:

1
2
3
static String secrect; // not persistent because of static
final String secrect = “Satish”; // not persistent because of final
transient String secrect; // not persistent because of transient

一般使用注解的方式比较多。

8.5. 声明大字段

@Lob: 声明某个字段为大字段。

1
2
@Lob
private String content;

更详细的声明:

1
2
3
4
5
6
@Lob
//指定 Lob 类型数据的获取策略, FetchType.EAGER 表示非延迟 加载,而 FetchType. LAZY 表示延迟加载 ;
@Basic(fetch = FetchType.EAGER)
//columnDefinition 属性指定数据表对应的 Lob 字段类型
@Column(name = "content", columnDefinition = "LONGTEXT NOT NULL")
private String content;

8.6. 创建枚举类型的字段

可以使用枚举类型的字段,不过枚举字段要用@Enumerated注解修饰。

1
2
3
4
5
6
7
8
9
public enum Gender {
    MALE("男性"),
    FEMALE("女性");

    private String value;
    Gender(String str){
        value=str;
    }
}
1
2
3
4
5
6
7
8
9
10
11
12
@Entity
@Table(name = "role")
public class Role {
    @Id
    @GeneratedValue(strategy = GenerationType.IDENTITY)
    private Long id;
    private String name;
    private String description;
    @Enumerated(EnumType.STRING)
    private Gender gender;
    省略getter/setter......
}

数据库里面对应存储的是 MAIL/FEMAIL。

8.7. 增加审计功能

只要继承了 AbstractAuditBase的类都会默认加上下面四个字段。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
@Data
@AllArgsConstructor
@NoArgsConstructor
@MappedSuperclass
@EntityListeners(value = AuditingEntityListener.class)
public abstract class AbstractAuditBase {

    @CreatedDate
    @Column(updatable = false)
    @JsonIgnore
    private Instant createdAt;

    @LastModifiedDate
    @JsonIgnore
    private Instant updatedAt;

    @CreatedBy
    @Column(updatable = false)
    @JsonIgnore
    private String createdBy;

    @LastModifiedBy
    @JsonIgnore
    private String updatedBy;
}

我们对应的审计功能对应地配置类可能是下面这样的(Spring Security 项目):

1
2
3
4
5
6
7
8
9
10
11
@Configuration
@EnableJpaAuditing
public class AuditSecurityConfiguration {
    @Bean
    AuditorAware<String> auditorAware() {
        return () -> Optional.ofNullable(SecurityContextHolder.getContext())
                .map(SecurityContext::getAuthentication)
                .filter(Authentication::isAuthenticated)
                .map(Authentication::getName);
    }
}

简单介绍一下上面设计到的一些注解:

  1. @CreatedDate: 表示该字段为创建时间时间字段,在这个实体被 insert 的时候,会设置值

  2. @CreatedBy : 表示该字段为创建人,在这个实体被 insert 的时候,会设置值

    @LastModifiedDate@LastModifiedBy同理。

@EnableJpaAuditing:开启 JPA 审计功能。

8.8. 删除 / 修改数据

@Modifying 注解提示 JPA 该操作是修改操作, 注意还要配合@Transactional注解使用。

1
2
3
4
5
6
7
@Repository
public interface UserRepository extends JpaRepository<User, Integer> {

    @Modifying
    @Transactional(rollbackFor = Exception.class)
    void deleteByUserName(String userName);
}

8.9. 关联关系

  • @OneToOne 声明一对一关系

  • @OneToMany 声明一对多关系

  • @ManyToOne声明多对一关系

  • MangToMang声明多对多关系

更多关于 Spring Boot JPA 的文章请看我的这篇文章:一文搞懂如何在 Spring Boot 正确中使用 JPA

9. 事务 @Transactional

在要开启事务的方法上使用@Transactional注解即可!

1
2
3
4
@Transactional(rollbackFor = Exception.class)
public void save() {
  ......
}

我们知道 Exception 分为运行时异常 RuntimeException 和非运行时异常。在@Transactional注解中如果不配置rollbackFor属性, 那么事物只会在遇到RuntimeException的时候才会回滚, 加上rollbackFor=Exception.class, 可以让事物在遇到非运行时异常时也回滚。

@Transactional 注解一般用在可以作用在或者方法上。

  • 作用于类:当把@Transactional 注解放在类上时,表示所有该类的public 方法都配置相同的事务属性信息。

  • 作用于方法:当类配置了@Transactional,方法也配置了@Transactional,方法的事务会覆盖类的事务配置信息。

更多关于关于 Spring 事务的内容请查看:

  1. 可能是最漂亮的 Spring 事务管理详解

  2. 一口气说出 6 种 @Transactional 注解失效场景

10. json 数据处理

10.1. 过滤 json 数据

@JsonIgnoreProperties 作用在类上用于过滤掉特定字段不返回或者不解析。

1
2
3
4
5
6
7
8
9
10
//生成json时将userRoles属性过滤
@JsonIgnoreProperties({"userRoles"})
public class User {

    private String userName;
    private String fullName;
    private String password;
    @JsonIgnore
    private List<UserRole> userRoles = new ArrayList<>();
}

@JsonIgnore一般用于类的属性上,作用和上面的@JsonIgnoreProperties 一样。

1
2
3
4
5
6
7
8
9
public class User {

    private String userName;
    private String fullName;
    private String password;
   //生成json时将userRoles属性过滤
    @JsonIgnore
    private List<UserRole> userRoles = new ArrayList<>();
}

10.2. 格式化 json 数据

@JsonFormat一般用来格式化 json 数据。:

比如:

1
2
@JsonFormat(shape=JsonFormat.Shape.STRING, pattern="yyyy-MM-dd'T'HH:mm:ss.SSS'Z'", timezone="GMT")
private Date date;

10.3. 扁平化对象

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
@Getter
@Setter
@ToString
public class Account {
    @JsonUnwrapped
    private Location location;
    @JsonUnwrapped
    private PersonInfo personInfo;

  @Getter
  @Setter
  @ToString
  public static class Location {
     private String provinceName;
     private String countyName;
  }
  @Getter
  @Setter
  @ToString
  public static class PersonInfo {
    private String userName;
    private String fullName;
  }
}

未扁平化之前:

1
2
3
4
5
6
7
8
9
10
{
    "location": {
        "provinceName":"湖北",
        "countyName":"武汉"
    },
    "personInfo": {
        "userName": "coder1234",
        "fullName": "shaungkou"
    }
}

使用@JsonUnwrapped 扁平对象之后:

1
2
3
4
5
6
7
8
9
10
@Getter
@Setter
@ToString
public class Account {
    @JsonUnwrapped
    private Location location;
    @JsonUnwrapped
    private PersonInfo personInfo;
    ......
}
1
2
3
4
5
6
{
  "provinceName":"湖北",
  "countyName":"武汉",
  "userName": "coder1234",
  "fullName": "shaungkou"
}

11. 测试相关

@ActiveProfiles一般作用于测试类上, 用于声明生效的 Spring 配置文件。

1
2
3
4
5
6
@SpringBootTest(webEnvironment = RANDOM_PORT)
@ActiveProfiles("test")
@Slf4j
public abstract class TestBase {
  ......
}

@Test声明一个方法为测试方法

@Transactional被声明的测试方法的数据会回滚,避免污染测试数据。

@WithMockUser Spring Security 提供的,用来模拟一个真实用户,并且可以赋予权限。

1
2
3
4
5
6
    @Test
    @Transactional
    @WithMockUser(username = "user-id-18163138155", authorities = "ROLE_TEACHER")
    void should_import_student_success() throws Exception {
        ......
    }

暂时总结到这里吧!虽然花了挺长时间才写完,不过可能还是会一些常用的注解的被漏掉,所以,我将文章也同步到了 Github 上去,Github 地址:_https://github.com/Snailclimb/JavaGuide/blob/master/docs/system-design/framework/spring/spring-annotations.md _欢迎完善!

看完的同学是不是想惊呼:“太棒了”。

本文已经收录进我的 75K Star 的 Java 开源项目 JavaGuide:https://github.com/Snailclimb/JavaGuide

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
推荐阅读
谈谈我从滴滴离开后的职业选择
看你简历上写熟悉 AIDL,说一说oneway吧
耐人寻味的 8 幅Java技术图










编程·思维·职场
欢迎扫码关注

转载,原文地址 https://mp.weixin.qq.com/s/FJHodWFi_-0eMZhrJVR9PA

示例表 A:

author_idauthor_name
1Kimmy
2Abel
3Bill
4Berton

示例表 B:

book_idauthor_idstart_dateend_date
912017-09-25 21:16:042017-09-25 21:16:06
103

1122017-09-25 21:21:462017-09-25 21:21:47
121

138

示例表 C:

order_idbook_idpriceorder_date
190.22017-09-24 21:21:46
290.62017-09-25 21:16:04
3110.12017-09-25 21:21:46

在以上表中执行 AB 表关联

1
2
SELECT `authors`.*, `books`.book_id FROM `authors`
LEFT JOIN `books` ON `authors`.author_id = `books`.author_id

结果

author_idauthor_namebook_id
1Kimmy9
3Bill10
2Abel11
1Kimmy12
4Berton

结果出现了 2 条 author_id 为 1 的记录,因为右表中存在了两条关联 author_id=1 的行

右边出现 N 条关联左边的记录,结果就会相应出现 N 条关联了右表出现的记录

在以上表中执行 ABC 表关联

1
2
3
SELECT `authors`.*, `books`.book_id, `orders`.order_id, `orders`.price FROM `authors`
LEFT JOIN `books` ON `authors`.author_id = `books`.author_id
LEFT JOIN `orders` ON `books`.book_id = `orders`.book_id

结果

author_idauthor_namebook_idorder_idorder_price
1Kimmy910.2
1Kimmy920.6
2Abel1130.1
3Bill10

1Kimmy12

4Berton


结果出现了 3 条 author_id=1 的记录,因为 authors 第一次关联了 books 表 book_id 为 9 和 12 的 book 关联了 author_id 为 1 的作者,而 book_id 为 9 的书本则关联了两个 orders 记录,所以结果集包含 3 条 author_id 为 1 的记录

可以运用

1
count(),sum()

等函数通过

1
group by

来统计结果

1
2
3
4
SELECT `authors`.*, sum(`orders`.price) FROM `authors`
LEFT JOIN `books` ON `authors`.author_id = `books`.author_id
LEFT JOIN `orders` ON `books`.book_id = `orders`.book_id
GROUP BY `books`.book_id

结果集会基于 book_id 来统计每一本书的订单总额

author_idauthor_namebook_idsum(order_price)
4Berton

1Kimmy90.80
3Bill10
2Abel110.10
1Kimmy12

book_id 为 9 的订单总额为 0.80,并且 9 的记录从多条合并为 1 条。

多条件 join

1
2
3
4
SELECT `authors`.*, `books`.book_id, `orders`.order_id, sum(`orders`.price) FROM `authors`
LEFT JOIN `books` ON `authors`.author_id = `books`.author_id
LEFT JOIN `orders` ON `books`.book_id = `orders`.book_id AND `orders`.order_date >= `books`.start_date AND `orders`.order_date <= `books`.end_date
GROUP BY `books`.book_id

选取在一定时间区间范围内的 order 订单,可以看到订单 order_id 为 1 的订单不再纳入 book_id 为 9 的统计当中,因为它的时间区间不符合 join 条件

author_idauthor_namebook_idorder_idsum(`order`.price)
4Berton


1Kimmy920.60
3Bill10

2Abel1130.10
1Kimmy12

关于 where 的使用,看下面示范

1
2
3
4
5
SELECT `authors`.*, `books`.book_id, `orders`.order_id, sum(`orders`.price) AS prices FROM `authors`
LEFT JOIN `books` ON `authors`.author_id = `books`.author_id
LEFT JOIN `orders` ON `books`.book_id = `orders`.book_id AND `orders`.order_date >= `books`.start_date AND `orders`.order_date <= `books`.end_date
WHERE prices is not NULL
GROUP BY `books`.book_id

以上语句假设选取 price 不为空的记录,导致了一个错误的出现

1
[Err] 1054 - Unknown column 'prices' in 'where clause'

因为 where 不能用于选取列的 AS 别名判断,MYSQL 的处理机制是先进行选取,再进行筛选,在选取阶段就启用了 where 条件,因为这时并不存在 prices 的筛选结果后才产生的字段,所以这里会抛出错误

我们可以这样做

1
2
3
4
5
SELECT `authors`.*, `books`.book_id, `orders`.order_id, sum(`orders`.price) AS prices FROM `authors`
LEFT JOIN `books` ON `authors`.author_id = `books`.author_id
LEFT JOIN `orders` ON `books`.book_id = `orders`.book_id AND `orders`.order_date >= `books`.start_date AND `orders`.order_date <= `books`.end_date
WHERE `orders`.price is not NULL
GROUP BY `books`.book_id

选取阶段 order 表是存在 price 字段的,所以只有 price 不为空的记录才会被选取

author_idauthor_namebook_idorder_idprices
2Abel1130.10
1Kimmy920.60

运用

1
having

对那些无法进行 WHERE 的 AS 别名的字段进行一些筛选查询

1
2
3
4
5
SELECT `authors`.*, `books`.book_id, sum(`orders`.price)AS prices FROM `authors`
LEFT JOIN `books` ON `authors`.author_id = `books`.author_id
LEFT JOIN `orders` ON `books`.book_id = `orders`.book_id
GROUP BY `books`.book_id
HAVING prices > 0.1

这时只有 sum 为 0.8 的结果被选中

author_idauthor_namebook_idsum(order_price)
1Kimmy90.80

对于组合其他语法查询,也是没问题的

1
2
3
4
5
6
7
SELECT `authors`.*, `books`.book_id, sum(`orders`.price)AS prices FROM `authors`
LEFT JOIN `books` ON `authors`.author_id = `books`.author_id
LEFT JOIN `orders` ON `books`.book_id = `orders`.book_id
GROUP BY `books`.book_id
HAVING prices >= 0.1
ORDER BY prices asc
LIMIT 1,1

转载,原文地址 https://mp.weixin.qq.com/s/cHA6m_e7yfd64aPaSqY0mg

线上故障主要会包括 CPU、磁盘、内存以及网络问题,而大多数故障可能会包含不止一个层面的问题,所以进行排查时候尽量四个方面依次排查一遍。同时例如 jstack、jmap 等工具也是不囿于一个方面的问题的,基本上出问题就是 df、free、top 三连,然后依次 jstack、jmap 伺候,具体问题具体分析即可。

CPU

一般来讲我们首先会排查 CPU 方面的问题。CPU 异常往往还是比较好定位的。原因包括业务逻辑问题 (死循环)、频繁 gc 以及上下文切换过多。而最常见的往往是业务逻辑(或者框架逻辑) 导致的,可以使用 jstack 来分析对应的堆栈情况。

使用 jstack 分析 CPU 问题

我们先用 ps 命令找到对应进程的 pid(如果你有好几个目标进程,可以先用 top 看一下哪个占用比较高)。

接着用 top -H -p pid 来找到 CPU 使用率比较高的一些线程

然后将占用最高的 pid 转换为 16 进制 printf ‘%x\n’ pid 得到 nid

接着直接在 jstack 中找到相应的堆栈信息 jstack pid |grep ‘nid’ -C5 –color

可以看到我们已经找到了 nid 为 0x42 的堆栈信息,接着只要仔细分析一番即可。

当然更常见的是我们对整个 jstack 文件进行分析,通常我们会比较关注 WAITING 和 TIMED_WAITING 的部分,BLOCKED 就不用说了。我们可以使用命令 cat jstack.log | grep “java.lang.Thread.State” | sort -nr | uniq -c 来对 jstack 的状态有一个整体的把握,如果 WAITING 之类的特别多,那么多半是有问题啦。

频繁 gc

当然我们还是会使用 jstack 来分析问题,但有时候我们可以先确定下 gc 是不是太频繁,使用 jstat -gc pid 1000 命令来对 gc 分代变化情况进行观察,1000 表示采样间隔 (ms),S0C/S1C、S0U/S1U、EC/EU、OC/OU、MC/MU 分别代表两个 Survivor 区、Eden 区、老年代、元数据区的容量和使用量。YGC/YGT、FGC/FGCT、GCT 则代表 YoungGc、FullGc 的耗时和次数以及总耗时。如果看到 gc 比较频繁,再针对 gc 方面做进一步分析,具体可以参考一下 gc 章节的描述。

上下文切换

针对频繁上下文问题,我们可以使用 vmstat 命令来进行查看

cs(context switch) 一列则代表了上下文切换的次数。

如果我们希望对特定的 pid 进行监控那么可以使用 pidstat -w pid 命令,cswch 和 nvcswch 表示自愿及非自愿切换。

磁盘

磁盘问题和 CPU 一样是属于比较基础的。首先是磁盘空间方面,我们直接使用 df -hl 来查看文件系统状态

更多时候,磁盘问题还是性能上的问题。我们可以通过 iostatiostat -d -k -x 来进行分析

最后一列 %util 可以看到每块磁盘写入的程度,而 rrqpm/s 以及 wrqm/s 分别表示读写速度,一般就能帮助定位到具体哪块磁盘出现问题了。

另外我们还需要知道是哪个进程在进行读写,一般来说开发自己心里有数,或者用 iotop 命令来进行定位文件读写的来源。

不过这边拿到的是 tid,我们要转换成 pid,可以通过 readlink 来找到 pidreadlink -f /proc/*/task/tid/../..。

找到 pid 之后就可以看这个进程具体的读写情况 cat /proc/pid/io

我们还可以通过 lsof 命令来确定具体的文件读写情况 lsof -p pid

内存

内存问题排查起来相对比 CPU 麻烦一些,场景也比较多。主要包括 OOM、GC 问题和堆外内存。一般来讲,我们会先用 free 命令先来检查一发内存的各种情况。

堆内内存

内存问题大多还都是堆内内存问题。表象上主要分为 OOM 和 Stack Overflo。

OOM

JMV 中的内存不足,OOM 大致可以分为以下几种:

Exception in thread “main” java.lang.OutOfMemoryError: unable to create new native thread

这个意思是没有足够的内存空间给线程分配 Java 栈,基本上还是线程池代码写的有问题,比如说忘记 shutdown,所以说应该首先从代码层面来寻找问题,使用 jstack 或者 jmap。如果一切都正常,JVM 方面可以通过指定 Xss 来减少单个 thread stack 的大小。另外也可以在系统层面,可以通过修改 / etc/security/limits.confnofile 和 nproc 来增大 os 对线程的限制

Exception in thread “main” java.lang.OutOfMemoryError: Java heap space

这个意思是堆的内存占用已经达到 - Xmx 设置的最大值,应该是最常见的 OOM 错误了。解决思路仍然是先应该在代码中找,怀疑存在内存泄漏,通过 jstack 和 jmap 去定位问题。如果说一切都正常,才需要通过调整 Xmx 的值来扩大内存。

Caused by: java.lang.OutOfMemoryError: Meta space

这个意思是元数据区的内存占用已经达到 XX:MaxMetaspaceSize 设置的最大值,排查思路和上面的一致,参数方面可以通过 XX:MaxPermSize 来进行调整 (这里就不说 1.8 以前的永久代了)。

Stack Overflow

栈内存溢出,这个大家见到也比较多。

Exception in thread “main” java.lang.StackOverflowError

表示线程栈需要的内存大于 Xss 值,同样也是先进行排查,参数方面通过 Xss 来调整,但调整的太大可能又会引起 OOM。

使用 JMAP 定位代码内存泄漏

上述关于 OOM 和 Stack Overflo 的代码排查方面,我们一般使用 JMAPjmap -dump:format=b,file=filename pid 来导出 dump 文件

通过 mat(Eclipse Memory Analysis Tools) 导入 dump 文件进行分析,内存泄漏问题一般我们直接选 Leak Suspects 即可,mat 给出了内存泄漏的建议。另外也可以选择 Top Consumers 来查看最大对象报告。和线程相关的问题可以选择 thread overview 进行分析。除此之外就是选择 Histogram 类概览来自己慢慢分析,大家可以搜搜 mat 的相关教程。

日常开发中,代码产生内存泄漏是比较常见的事,并且比较隐蔽,需要开发者更加关注细节。比如说每次请求都 new 对象,导致大量重复创建对象;进行文件流操作但未正确关闭;手动不当触发 gc;ByteBuffer 缓存分配不合理等都会造成代码 OOM。

另一方面,我们可以在启动参数中指定 - XX:+HeapDumpOnOutOfMemoryError 来保存 OOM 时的 dump 文件。

gc 问题和线程

gc 问题除了影响 CPU 也会影响内存,排查思路也是一致的。一般先使用 jstat 来查看分代变化情况,比如 youngGC 或者 fullGC 次数是不是太多呀;EU、OU 等指标增长是不是异常呀等。

线程的话太多而且不被及时 gc 也会引发 oom,大部分就是之前说的 unable to create new native thread。除了 jstack 细细分析 dump 文件外,我们一般先会看下总体线程,通过 pstreee -p pid |wc -l。

或者直接通过查看 / proc/pid/task 的数量即为线程数量。

堆外内存

如果碰到堆外内存溢出,那可真是太不幸了。首先堆外内存溢出表现就是物理常驻内存增长快,报错的话视使用方式都不确定,如果由于使用 Netty 导致的,那错误日志里可能会出现 OutOfDirectMemoryError 错误,如果直接是 DirectByteBuffer,那会报 OutOfMemoryError: Direct buffer memory。

堆外内存溢出往往是和 NIO 的使用相关,一般我们先通过 pmap 来查看下进程占用的内存情况 pmap -x pid | sort -rn -k3 | head -30,这段意思是查看对应 pid 倒序前 30 大的内存段。这边可以再一段时间后再跑一次命令看看内存增长情况,或者和正常机器比较可疑的内存段在哪里。

我们如果确定有可疑的内存端,需要通过 gdb 来分析 gdb –batch –pid {pid} -ex “dump memory filename.dump {内存起始地址} {内存起始地址 + 内存块大小}”

获取 dump 文件后可用 heaxdump 进行查看 hexdump -C filename | less,不过大多数看到的都是二进制乱码。

NMT 是 Java7U40 引入的 HotSpot 新特性,配合 jcmd 命令我们就可以看到具体内存组成了。需要在启动参数中加入 -XX:NativeMemoryTracking=summary 或者 -XX:NativeMemoryTracking=detail,会有略微性能损耗。

一般对于堆外内存缓慢增长直到爆炸的情况来说,可以先设一个基线 jcmd pid VM.native_memory baseline。

然后等放一段时间后再去看看内存增长的情况,通过 jcmd pid VM.native_memory detail.diff(summary.diff) 做一下 summary 或者 detail 级别的 diff。

可以看到 jcmd 分析出来的内存十分详细,包括堆内、线程以及 gc(所以上述其他内存异常其实都可以用 nmt 来分析),这边堆外内存我们重点关注 Internal 的内存增长,如果增长十分明显的话那就是有问题了。

detail 级别的话还会有具体内存段的增长情况,如下图。

此外在系统层面,我们还可以使用 strace 命令来监控内存分配 strace -f -e “brk,mmap,munmap” -p pid

这边内存分配信息主要包括了 pid 和内存地址。

不过其实上面那些操作也很难定位到具体的问题点,关键还是要看错误日志栈,找到可疑的对象,搞清楚它的回收机制,然后去分析对应的对象。比如 DirectByteBuffer 分配内存的话,是需要 full GC 或者手动 system.gc 来进行回收的 (所以最好不要使用 - XX:+DisableExplicitGC)。那么其实我们可以跟踪一下 DirectByteBuffer 对象的内存情况,通过 jmap -histo:live pid 手动触发 fullGC 来看看堆外内存有没有被回收。如果被回收了,那么大概率是堆外内存本身分配的太小了,通过 - XX:MaxDirectMemorySize 进行调整。如果没有什么变化,那就要使用 jmap 去分析那些不能被 gc 的对象,以及和 DirectByteBuffer 之间的引用关系了。

GC 问题

堆内内存泄漏总是和 GC 异常相伴。不过 GC 问题不只是和内存问题相关,还有可能引起 CPU 负载、网络问题等系列并发症,只是相对来说和内存联系紧密些,所以我们在此单独总结一下 GC 相关问题。

我们在 CPU 章介绍了使用 jstat 来获取当前 GC 分代变化信息。而更多时候,我们是通过 GC 日志来排查问题的,在启动参数中加上 - verbose:gc -XX:+PrintGCDetails -XX:+PrintGCDateStamps -XX:+PrintGCTimeStamps 来开启 GC 日志。

常见的 Young GC、Full GC 日志含义在此就不做赘述了。

针对 gc 日志,我们就能大致推断出 youngGC 与 fullGC 是否过于频繁或者耗时过长,从而对症下药。我们下面将对 G1 垃圾收集器来做分析,这边也建议大家使用 G1-XX:+UseG1GC。

youngGC 过频繁

youngGC 频繁一般是短周期小对象较多,先考虑是不是 Eden 区 / 新生代设置的太小了,看能否通过调整 - Xmn、-XX:SurvivorRatio 等参数设置来解决问题。如果参数正常,但是 young gc 频率还是太高,就需要使用 Jmap 和 MAT 对 dump 文件进行进一步排查了。

youngGC 耗时过长

耗时过长问题就要看 GC 日志里耗时耗在哪一块了。以 G1 日志为例,可以关注 Root Scanning、Object Copy、Ref Proc 等阶段。Ref Proc 耗时长,就要注意引用相关的对象。Root Scanning 耗时长,就要注意线程数、跨代引用。Object Copy 则需要关注对象生存周期。而且耗时分析它需要横向比较,就是和其他项目或者正常时间段的耗时比较。比如说图中的 Root Scanning 和正常时间段比增长较多,那就是起的线程太多了。

触发 fullGC

G1 中更多的还是 mixedGC,但 mixedGC 可以和 youngGC 思路一样去排查。触发 fullGC 了一般都会有问题,G1 会退化使用 Serial 收集器来完成垃圾的清理工作,暂停时长达到秒级别,可以说是半跪了。

fullGC 的原因可能包括以下这些,以及参数调整方面的一些思路:

  • 并发阶段失败:在并发标记阶段,MixGC 之前老年代就被填满了,那么这时候 G1 就会放弃标记周期。这种情况,可能就需要增加堆大小,或者调整并发标记线程数 - XX:ConcGCThreads。

  • 晋升失败:在 GC 的时候没有足够的内存供存活 / 晋升对象使用,所以触发了 Full GC。这时候可以通过 - XX:G1ReservePercent 来增加预留内存百分比,减少 - XX:InitiatingHeapOccupancyPercent 来提前启动标记,-XX:ConcGCThreads 来增加标记线程数也是可以的。

  • 大对象分配失败:大对象找不到合适的 region 空间进行分配,就会进行 fullGC,这种情况下可以增大内存或者增大 - XX:G1HeapRegionSize。

  • 程序主动执行 System.gc():不要随便写就对了。

另外,我们可以在启动参数中配置 - XX:HeapDumpPath=/xxx/dump.hprof 来 dump fullGC 相关的文件,并通过 jinfo 来进行 gc 前后的 dump

jinfo -flag +HeapDumpBeforeFullGC pid

jinfo -flag +HeapDumpAfterFullGC pid

jinfo -flag +HeapDumpBeforeFullGC pid

jinfo -flag +HeapDumpAfterFullGC pid

这样得到 2 份 dump 文件,对比后主要关注被 gc 掉的问题对象来定位问题。

网络

涉及到网络层面的问题一般都比较复杂,场景多,定位难,成为了大多数开发的噩梦,应该是最复杂的了。这里会举一些例子,并从 tcp 层、应用层以及工具的使用等方面进行阐述。

超时

超时错误大部分处在应用层面,所以这块着重理解概念。超时大体可以分为连接超时和读写超时,某些使用连接池的客户端框架还会存在获取连接超时和空闲连接清理超时。

  • 读写超时。readTimeout/writeTimeout,有些框架叫做 so_timeout 或者 socketTimeout,均指的是数据读写超时。注意这边的超时大部分是指逻辑上的超时。soa 的超时指的也是读超时。读写超时一般都只针对客户端设置。

  • 连接超时。connectionTimeout,客户端通常指与服务端建立连接的最大时间。服务端这边 connectionTimeout 就有些五花八门了,Jetty 中表示空闲连接清理时间,Tomcat 则表示连接维持的最大时间。

  • 其他。包括连接获取超时 connectionAcquireTimeout 和空闲连接清理超时 idleConnectionTimeout。多用于使用连接池或队列的客户端或服务端框架。

我们在设置各种超时时间中,需要确认的是尽量保持客户端的超时小于服务端的超时,以保证连接正常结束。

在实际开发中,我们关心最多的应该是接口的读写超时了。

如何设置合理的接口超时是一个问题。如果接口超时设置的过长,那么有可能会过多地占用服务端的 tcp 连接。而如果接口设置的过短,那么接口超时就会非常频繁。

服务端接口明明 rt 降低,但客户端仍然一直超时又是另一个问题。这个问题其实很简单,客户端到服务端的链路包括网络传输、排队以及服务处理等,每一个环节都可能是耗时的原因。

TCP 队列溢出

tcp 队列溢出是个相对底层的错误,它可能会造成超时、rst 等更表层的错误。因此错误也更隐蔽,所以我们单独说一说。

如上图所示,这里有两个队列:syns queue(半连接队列)、accept queue(全连接队列)。三次握手,在 server 收到 client 的 syn 后,把消息放到 syns queue,回复 syn+ack 给 client,server 收到 client 的 ack,如果这时 accept queue 没满,那就从 syns queue 拿出暂存的信息放入 accept queue 中,否则按 tcp_abort_on_overflow 指示的执行。

tcp_abort_on_overflow 0 表示如果三次握手第三步的时候 accept queue 满了那么 server 扔掉 client 发过来的 ack。tcp_abort_on_overflow 1 则表示第三步的时候如果全连接队列满了,server 发送一个 rst 包给 client,表示废掉这个握手过程和这个连接,意味着日志里可能会有很多 connection reset / connection reset by peer。

那么在实际开发中,我们怎么能快速定位到 tcp 队列溢出呢?

netstat 命令,执行 netstat -s | egrep “listen|LISTEN”

如上图所示,overflowed 表示全连接队列溢出的次数,sockets dropped 表示半连接队列溢出的次数。

ss 命令,执行 ss -lnt

上面看到 Send-Q 表示第三列的 listen 端口上的全连接队列最大为 5,第一列 Recv-Q 为全连接队列当前使用了多少。

接着我们看看怎么设置全连接、半连接队列大小吧:

全连接队列的大小取决于 min(backlog, somaxconn)。backlog 是在 socket 创建的时候传入的,somaxconn 是一个 os 级别的系统参数。而半连接队列的大小取决于 max(64, /proc/sys/net/ipv4/tcp_max_syn_backlog)。

在日常开发中,我们往往使用 servlet 容器作为服务端,所以我们有时候也需要关注容器的连接队列大小。在 Tomcat 中 backlog 叫做 acceptCount,在 Jetty 里面则是 acceptQueueSize。

RST 异常

RST 包表示连接重置,用于关闭一些无用的连接,通常表示异常关闭,区别于四次挥手。

在实际开发中,我们往往会看到 connection reset / connection reset by peer 错误,这种情况就是 RST 包导致的。

端口不存在

如果像不存在的端口发出建立连接 SYN 请求,那么服务端发现自己并没有这个端口则会直接返回一个 RST 报文,用于中断连接。

主动代替 FIN 终止连接

一般来说,正常的连接关闭都是需要通过 FIN 报文实现,然而我们也可以用 RST 报文来代替 FIN,表示直接终止连接。实际开发中,可设置 SO_LINGER 数值来控制,这种往往是故意的,来跳过 TIMED_WAIT,提供交互效率,不闲就慎用。

客户端或服务端有一边发生了异常,该方向对端发送 RST 以告知关闭连接

我们上面讲的 tcp 队列溢出发送 RST 包其实也是属于这一种。这种往往是由于某些原因,一方无法再能正常处理请求连接了 (比如程序崩了,队列满了),从而告知另一方关闭连接。

接收到的 TCP 报文不在已知的 TCP 连接内

比如,一方机器由于网络实在太差 TCP 报文失踪了,另一方关闭了该连接,然后过了许久收到了之前失踪的 TCP 报文,但由于对应的 TCP 连接已不存在,那么会直接发一个 RST 包以便开启新的连接。

一方长期未收到另一方的确认报文,在一定时间或重传次数后发出 RST 报文

这种大多也和网络环境相关了,网络环境差可能会导致更多的 RST 报文。

之前说过 RST 报文多会导致程序报错,在一个已关闭的连接上读操作会报 connection reset,而在一个已关闭的连接上写操作则会报 connection reset by peer。通常我们可能还会看到 broken pipe 错误,这是管道层面的错误,表示对已关闭的管道进行读写,往往是在收到 RST,报出 connection reset 错后继续读写数据报的错,这个在 glibc 源码注释中也有介绍。

我们在排查故障时候怎么确定有 RST 包的存在呢?当然是使用 tcpdump 命令进行抓包,并使用 wireshark 进行简单分析了。tcpdump -i en0 tcp -w xxx.cap,en0 表示监听的网卡。

接下来我们通过 wireshark 打开抓到的包,可能就能看到如下图所示,红色的就表示 RST 包了。

TIME_WAIT 和 CLOSE_WAIT

TIME_WAIT 和 CLOSE_WAIT 是啥意思相信大家都知道。

在线上时,我们可以直接用命令 netstat -n | awk ‘/^tcp/ {++S[$NF]} END {for(a in S) print a, S[a]}’来查看 time-wait 和 close_wait 的数量

用 ss 命令会更快 ss -ant | awk ‘{++S[$1]} END {for(a in S) print a, S[a]}’

TIME_WAIT

time_wait 的存在一是为了丢失的数据包被后面连接复用,二是为了在 2MSL 的时间范围内正常关闭连接。它的存在其实会大大减少 RST 包的出现。

过多的 time_wait 在短连接频繁的场景比较容易出现。这种情况可以在服务端做一些内核参数调优:

#表示开启重用。允许将 TIME-WAIT sockets 重新用于新的 TCP 连接,默认为 0,表示关闭

net.ipv4.tcp_tw_reuse = 1

#表示开启 TCP 连接中 TIME-WAIT sockets 的快速回收,默认为 0,表示关闭

net.ipv4.tcp_tw_recycle = 1

#表示开启重用。允许将 TIME-WAIT sockets 重新用于新的 TCP 连接,默认为 0,表示关闭

net.ipv4.tcp_tw_reuse = 1

#表示开启 TCP 连接中 TIME-WAIT sockets 的快速回收,默认为 0,表示关闭

net.ipv4.tcp_tw_recycle = 1

当然我们不要忘记在 NAT 环境下因为时间戳错乱导致数据包被拒绝的坑了,另外的办法就是改小 tcp_max_tw_buckets,超过这个数的 time_wait 都会被干掉,不过这也会导致报 time wait bucket table overflow 的错。

CLOSE_WAIT

close_wait 往往都是因为应用程序写的有问题,没有在 ACK 后再次发起 FIN 报文。close_wait 出现的概率甚至比 time_wait 要更高,后果也更严重。往往是由于某个地方阻塞住了,没有正常关闭连接,从而渐渐地消耗完所有的线程。

想要定位这类问题,最好是通过 jstack 来分析线程堆栈来排查问题,具体可参考上述章节。这里仅举一个例子。

开发同学说应用上线后 CLOSE_WAIT 就一直增多,直到挂掉为止,jstack 后找到比较可疑的堆栈是大部分线程都卡在了 countdownlatch.await 方法,找开发同学了解后得知使用了多线程但是确没有 catch 异常,修改后发现异常仅仅是最简单的升级 sdk 后常出现的 class not found。

想知道更多?****描下面的二维码关注我

后台回复” 加群 “获取公众号专属群聊入口

【原创系列 | 精彩推荐】

朕已阅